//
// Created by martin on 8/28/21.
//

#include <fcntl.h>
#include <stdio.h>
#include <event2/event.h>
#include <event2/bufferevent.h>
#include <string.h>
#include <event2/buffer.h>
#include <sys/stat.h>
#include <event2/util.h>
#include <assert.h>

#include "RETRaction.h"
#include "socket.h"
#include "../session.h"
#include "cmdaction.h"
#include "error.h"
#include "utils.h"

#define MB_SIZE ((int64_t)(1024 * 1024 * 1)) // 1MB

static void data_conn_event_cb(struct bufferevent *bev, short what, void *ctx);
static void data_conn_write_cb(struct bufferevent *bev, void *ctx);
static void RETR_close(Session *sess);

void data_conn_event_cb(struct bufferevent *bev, short what, void *ctx)
{
    Session *sess = (Session *)ctx;
    if (what & BEV_EVENT_CONNECTED) {
        printf("RETR data_conn_event_cb BEV_EVENT_CONNECTED\n");
        send_reply(sess->bev, 125, "Data connection already opened; transfer starting.");

        if (sess->binary_flag != 0) { /* binary mode */
            ssize_t f_size;
            struct evbuffer *evbuf;
            struct evbuffer_file_segment *segment;

            f_size = utils_getfilesize(sess->send_data_fd);
            if (f_size < 0) {
                goto errout;
            }

            evbuf = bufferevent_get_output(bev);
            segment = evbuffer_file_segment_new(sess->send_data_fd, sess->f_offset, f_size, EVBUF_FS_CLOSE_ON_FREE | EVBUF_FS_DISABLE_LOCKING);
            if (!segment) {
                goto errout;
            }

            sess->evbuf = evbuf;
            sess->segment = segment;

            // force use sendfile
            evbuffer_set_flags(evbuf, EVBUFFER_FLAG_DRAINS_TO_FD); /* open default */
        }

        bufferevent_trigger(sess->data_bev, EV_WRITE, 0);
    }
    else if (what & BEV_EVENT_EOF){
        printf("RETR data_conn_event_cb BEV_EVENT_EOF\n");
        RETR_close(sess);
    }
    else if (what & BEV_EVENT_ERROR){
        printf("RETR data_conn_event_cb BEV_EVENT_ERROR\n");
        RETR_close(sess);
    }
    else if (what & BEV_EVENT_TIMEOUT){
        printf("RETR data_conn_event_cb BEV_EVENT_TIMEOUT\n");
        send_reply(sess->bev, 426, "Timeout. Connection closed.");
        RETR_close(sess);
    }
    return;

errout:
    printf("RETR data_conn_event_cb errout\n");
    send_reply(bev, 451, "Requested action aborted: local error in processing.");
    RETR_close(sess);
}

void data_conn_write_cb(struct bufferevent *bev, void *ctx)
{
    Session *sess = (Session *)ctx;
    int fd = sess->send_data_fd;
    if (fd < 0) {
        goto errout;
    }
    
    if (sess->binary_flag == 0) { /* ASCII mode */
        char buf[1024];
        char new_buf[2048];
        ssize_t nread;
        ssize_t i, j;

        nread = read(fd, buf, sizeof(buf));
        for (i = 0, j= 0; i < nread; ++i) {
            if (buf[i] == '\n') {
                if (i == 0 || (i > 0 && buf[i - 1] != '\r')) {
                    new_buf[j++] = '\r';
                    new_buf[j++] = '\n';
                }
            }
            else {
                new_buf[j++] = buf[i];
            }
        }
        bufferevent_write(sess->data_bev, new_buf, j);

        if (nread > 0)
            return; /* wait next callback */
    }
    else { /* Binary mode */
        struct evbuffer *evbuf = sess->evbuf;
        struct evbuffer_file_segment *segment = sess->segment;
        int64_t f_offset = sess->f_offset;
        int64_t s_offset = sess->s_offset;
        int64_t f_size = utils_getfilesize(fd);
        int ret;

        if (evbuf == NULL || segment == NULL) {
            goto errout;
        }

        if (f_offset + s_offset < f_size) {
            size_t olen = evbuffer_get_length(evbuf);
            off_t rlen = (f_size - f_offset - s_offset > MB_SIZE) ? (MB_SIZE) : (f_size - f_offset - s_offset);

            ret = evbuffer_add_file_segment(evbuf, segment, s_offset, rlen);
            if (ret < 0) {
                goto errout;
            }

            s_offset += (int64_t)(evbuffer_get_length(evbuf) - olen);
            sess->f_offset = f_offset;
            sess->s_offset = s_offset;

#if DEBUG_DETAIL_LOG_EN
           printf("evlen: %zd, s_offset: %ld\n", evbuffer_get_length(evbuf), s_offset);
#endif
            return; /* wait next callback */
        }
    }

    // 此时, 数据传输完成
    RETR_close(sess);
    // 客户端接收226以后， 会close连接
    send_reply(sess->bev, 226, "Transfer complete.");
    return;

errout:
    RETR_close(sess);
    send_reply(bev, 451, "Requested action aborted: local error in processing.");
}

static void RETR_close(Session *sess)
{
    printf("RETR_close\n");
    if (!sess) return;
    if (sess->data_bev) {
        bufferevent_free(sess->data_bev);
        sess->data_bev = NULL;
    }

    if (sess->send_data_fd >= 0) {
        close(sess->send_data_fd);
        sess->send_data_fd = -1;
    }

    sess->f_offset = 0;
    sess->s_offset = 0;

    if (sess->evbuf) {
        sess->evbuf = NULL;
    }
    if (sess->segment) {
        evbuffer_file_segment_free(sess->segment);
        sess->segment = NULL;
    }

    if (sess->offset > 0) {
        sess->offset = 0;
    }
}

/**
 * 处理文件下载命令
 * @param sess
 * @return
 */
int RETR_action(Session *sess)
{
    if (!sess) return -1;

    // 检查文件是否存在
    if (!dir_exist(sess->dir, sess->cmd_text)) {
        send_reply(sess->bev, 550, "Requested action not taken; file unavailable.");
        return -1;
    }

    // 获取待下载文件物理路径
    char file_path_phy[PATH_MAX];
    if (dir_getPhysicalPath(sess->dir, sess->cmd_text, file_path_phy, sizeof(file_path_phy)) < 0) {
        send_reply(sess->bev, 550, "Requested action not taken; file unavailable.");
        return -1;
    }

    // TODO 获取读锁（共享锁） -- 文件忙
    // 打开文件
    int fd;
    if ((fd = open(file_path_phy, O_RDONLY | O_NONBLOCK)) < 0) {
        send_reply(sess->bev, 550, "Requested action not taken; file unavailable.");
        return -1;
    }

    // 如果有偏移offset > 0 (该offset由客户端命令指定)，就移动文件读写指针位置, 并且清空保存的偏移
    if (sess->offset > 0) {
        if(lseek(fd, sess->offset, SEEK_SET) < 0) {
            err_ret("lseek error");
            sess->offset = 0;
            return -1;
        }
        sess->offset = 0;
    }
    sess->send_data_fd = fd;

    // PORT模式连接客户端
    if (connect_PORT(sess, NULL, data_conn_write_cb, data_conn_event_cb) < 0) {
        send_reply(sess->data_bev, 425, "Can't open data connection.");
        return -1;
    }
    return 0;
}
